# -*- coding: utf-8 -*-

from __future__ import division
import os
import errno
import time
import select
import numbers
import functools
import itertools
import datetime
import heapq
import stack_context
from dm_py_util import DEBUG_LOG, ERROR_LOG, INFO_LOG
from engine.trace_back import fn
from engine.io_util import errno_from_exception, timedelta_to_seconds


class TimeoutCallback(object):
    '''延时事件'''

    def __init__(self, deadline, callback, io_scheduler):
        '''
        tiebreaker, timeout event id
        '''
        if not isinstance(deadline, numbers.Real):
            raise TypeError('Unsupported deadline %r' % deadline)
        self.deadline = deadline
        self.callback = callback
        self.tiebreaker = next(io_scheduler._timeout_counter)

    def __lt__(self, other):
        return ((self.deadline, self.tiebreaker) <
                (other.deadline, other.tiebreaker))

    def __le__(self, other):
        return ((self.deadline, self.tiebreaker) <=
                (other.deadline, other.tiebreaker))


class PeriodicCallback(object):
    '''
    周期事件
    '''
    def __init__(self, callback, callback_time, io_scheduler):
        self.callback = callback
        if callback_time <= 0:
            raise ValueError('Periodic callback must have a positive callback_time')
        if not io_scheduler:
            raise ValueError('io_scheduler must not be None')
        self.callback_time = callback_time
        self.io_scheduler = io_scheduler
        self._running = False
        self._timeout = None

    def start(self):
        '''Starts the timer.'''
        self._running = True
        self._next_timeout = self.io_scheduler.time()
        self._schedule_next()

    def stop(self):
        '''Stops the timer.'''
        self._running = False
        if self._timeout is not None:
            self.io_scheduler.remove_timeout(self._timeout)
            self._timeout = None

    def _run(self):
        if not self._running:
            return
        try:
            self.callback()
        except Exception:
            self.io_scheduler.handle_callback_exception(self.callback)
        self._schedule_next()

    def _schedule_next(self):
        if self._running:
            current_time = self.io_scheduler.time()
            while self._next_timeout <= current_time:
                self._next_timeout += self.callback_time / 1000.0
            self._timeout = self.io_scheduler.add_timeout(self._next_timeout, self._run)


class IOScheduler(object):     # TODO长时间任务阻塞
    '''
    IO调度器
    '''
    READ = select.EPOLLIN
    WRITE = select.EPOLLOUT
    ERROR = select.EPOLLERR | select.EPOLLHUP
    POLL_TIMEOUT = 3600.0
    def __init__(self, idle_callback=None):
        self._idle_callback = idle_callback

    def initialize(self):
        self._impl = select.epoll()
        self._handlers = {}
        self._events = {}
        self._callbacks = []
        self._timeouts = []
        self._cancellations = 0
        self._running = False
        self._stopped = False
        self._closing = False
        self._timeout_counter = itertools.count()

    def close(self, all_fds=False):
        self._closing = True
        if all_fds:
            for fd, handler in self._handlers.values():
                self.close_fd(fd)
        self._impl.close()
        self._callbacks = None
        self._timeouts = None

    def add_handler(self, fd, handler, events):
        fd, obj = self.split_fd(fd)
        self._handlers[fd] = (obj, stack_context.wrap(handler))
        self._impl.register(fd, events | self.ERROR | select.EPOLLET)

    def update_handler(self, fd, events):
        fd, obj = self.split_fd(fd)
        self._impl.modify(fd, events | self.ERROR | select.EPOLLET)

    def remove_handler(self, fd):
        fd, obj = self.split_fd(fd)
        self._handlers.pop(fd, None)
        self._events.pop(fd, None)
        try:
            self._impl.unregister(fd)
        except Exception as e:
            DEBUG_LOG("Error deleting fd from IOScheduler", e)

    def time(self):
        return time.time()

    def add_timeout(self, deadline, callback, *args, **kwargs):
        if isinstance(deadline, numbers.Real):
            return self.call_at(deadline, callback, *args, **kwargs)
        elif isinstance(deadline, datetime.timedelta):
            return self.call_at(self.time() + timedelta_to_seconds(deadline),
                                callback, *args, **kwargs)
        else:
            raise TypeError("Unsupported deadline %r" % deadline)

    def set_timeout(self, callback, deadline, *args, **kwargs):
        return self.add_timeout(self.time() + deadline / 1000, callback, *args, **kwargs)

    def call_later(self, delay, callback, *args, **kwargs):
        self.call_at(self.time() + delay, callback, *args, **kwargs)

    def call_at(self, deadline, callback, *args, **kwargs):
        timeout = TimeoutCallback(
            deadline,
            functools.partial(stack_context.wrap(callback), *args, **kwargs),
            self)
        heapq.heappush(self._timeouts, timeout)
        return timeout

    def remove_timeout(self, timeout):
        timeout.callback = None
        self._cancellations += 1

    def add_callback(self, callback, *args, **kwargs):
        self._callbacks.append(functools.partial(
            stack_context.wrap(callback), *args, **kwargs))

    def handle_callback_exception(self, callback):
        ERROR_LOG('Exception in callback', callback, fn())

    def split_fd(self, fd):
        try:
            return fd.fileno(), fd
        except AttributeError:
            return fd, fd

    def close_fd(self, fd):
        try:
            try:
                fd.close()
            except AttributeError:
                os.close(fd)
        except OSError:
            pass

    def process_once(self, poll_wait=POLL_TIMEOUT):
        '''
        处理一次epoll
        '''
        callbacks = self._callbacks
        self._callbacks = []
        if self._timeouts:
            now = self.time()
            while self._timeouts:
                if self._timeouts[0].callback is None:
                    # the timeout was cancelled
                    heapq.heappop(self._timeouts)
                    self._cancellations -= 1
                elif self._timeouts[0].deadline <= now:
                    timeout = heapq.heappop(self._timeouts)
                    callbacks.append(timeout.callback)
                    del timeout
                else:
                    break
            if (self._cancellations > 512
                    and self._cancellations > (len(self._timeouts) >> 1)):
                # Clean up the timeout queue when it gets large and it's
                # more than half cancellations.
                self._cancellations = 0
                self._timeouts = [x for x in self._timeouts
                                    if x.callback is not None]
                heapq.heapify(self._timeouts)
            for callback in callbacks:
                self._run_callback(callback)
            # Closures may be holding on to a lot of memory, so allow
            # them to be freed before we go into our poll wait.
            callbacks = callback = None
            if self._callbacks:
                # If any callbacks or timeouts called add_callback,
                # we don't want to wait in poll() before we run them.
                poll_timeout = 0.0
            elif self._timeouts:
                # If there are any timeouts, schedule the first one.
                # Use self.time() instead of 'now' to account for time
                # spent running callbacks.
                poll_timeout = self._timeouts[0].deadline - self.time()
                poll_timeout = max(0, min(poll_timeout, self.POLL_TIMEOUT, poll_wait))
            else:
                # No timeouts and no callbacks, so use the default.
                poll_timeout = self.POLL_TIMEOUT
            if not self._running:
                return
            try:
                event_pairs = self._impl.poll(poll_timeout)
            except Exception as e:
                # two ways EINTR might be signaled:
                # * e.errno == errno.EINTR
                # * e.args is like (errno.EINTR, 'Interrupted system call')
                if errno_from_exception(e) == errno.EINTR:
                    return
                else:
                    raise
            self._events.update(event_pairs)
            while self._events:
                fd, events = self._events.popitem()
                try:
                    fd_obj, handler_func = self._handlers[fd]
                    handler_func(fd_obj, events)
                except (OSError, IOError) as e:
                    if errno_from_exception(e) == errno.EPIPE:
                        # Happens when the other closes the connection
                        pass
                    else:
                        self.handle_callback_exception(self._handlers.get(fd))
                except Exception:
                    self.handle_callback_exception(self._handlers.get(fd))
            fd_obj = handler_func = None
        else:
            self.process_idle()

    def process_idle(self):
        try:
            if self._idle_callback and callable(self._idle_callback):
                self._idle_callback()
        except Exception:
            self.handle_callback_exception(self._idle_callback)

    def _run_callback(self, callback):
        try:
            callback()
        except Exception:
            self.handle_callback_exception(callback)

    def run_forever(self):
        '''
        测试使用
        '''
        if self._running:
            raise RuntimeError("IOScheduler is already running")
        if self._stopped:
            self._stopped = False
            return
        self._running = True
        try:
            while True:
                self.process_once()
        finally:
            self._stopped = False




#if __name__ == '__main__':
#    cc = IOScheduler('1234', '2342', ds='234', tt='234')
#    cc.initialize()
#    def print_now():
#        print 'now time', time.time()
#    print_now()
#    cc.set_timeout(print_now, 1)
#    pc = PeriodicCallback(print_now, 5000, cc)
#    pc.start()
#    from push_client import PushClient
#    config_file = './push_client.config'
#    push_client = PushClient('123')
#    push_client.init(config_file)
#    push_client.build()
#    def on_process(fd, event):
#        print event, cc.READ , cc.WRITE, event & cc.READ and event & cc.WRITE, select.EPOLLIN,  select.EPOLLOUT
#        if (event & cc.READ) and (event & cc.WRITE):
#            event_str = 'RW'
#        elif event & cc.READ:
#            event_str = 'READ'
#        elif event & cc.WRITE:
#            event_str = 'WRITE'
#        print fd, event_str
#        cc.remove_handler(fd)
#    cc.add_handler(push_client, on_process, cc.READ | cc.WRITE | select.EPOLLET)
#    cc.run_forever()
